package com.spzx.common.redis.util;

import com.spzx.common.core.utils.ThreadPoolUtils;
import com.spzx.common.core.utils.uuid.UUID;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//可重入的分布式锁
@Component
@Slf4j
public class RedisLockUtil {
    @Resource
    StringRedisTemplate stringRedisTemplate;
    //获取分布式锁方法
    public void lock(String lockKey, String uuid, int timeout) {
        //获取重入锁
//        String script = " if redis.call('exists' , KEYS[1]) " + //锁存在
//                " then " +
//                //判断锁是不是自己的
//                "   if redis.call('hexists' , KEYS[1] , ARGV[1])   " +//锁是自己的，表示锁重入了
//                "     then return redis.call('hincrby' ,KEYS[1] , ARGV[1] , 1)   " +//重入次数+1
//                "   else " + //锁不是自己的 ，不能操作锁
//                "       return 0 end " +
//                " else " + //第一次获取锁： 需要给锁设置过期时间、锁的重入次数默认为1
//                "      redis.call('hset' , KEYS[1] , ARGV[1] , 1 ) " +
//                "      redis.call('expire' , KEYS[1] , ARGV[2] )" +
//                "    return  1 " +
//                " end ";
        /*
            1、先判断锁是否被使用
                如果未被使用，第一次获取锁  并设置过期时间
            2、如果锁被使用：
                再判断锁是不是自己的：
                    如果是自己的 更新锁的使用次数(获取次数+1)
                如果不是自己的：
                    获取失败
         */
        String script = "if redis.call('exists' , KEYS[1])==0 " + //锁未被使用 直接获取锁使用
                "   then " +
                "       redis.call('hset' , KEYS[1] , ARGV[1] , 1 )" +
                "       redis.call('expire' , KEYS[1] , ARGV[2] )" +
                "       return 1 " +
                " elseif redis.call('hexists' , KEYS[1] , ARGV[1])==1" + //锁被自己使用 重入次数+1
                "   then " +
                "       redis.call('hincrby' , KEYS[1] , ARGV[1] , 1)" +
                "       return 2 " +
                " else " + //锁被使用 不是自己的锁 获取锁失败
                "   return 0 " +
                " end";
        Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockKey)
                , uuid, timeout + "");
        while(result.intValue()==0){
            try {
                TimeUnit.SECONDS.sleep(80+new Random().nextInt(100));
                result = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockKey)
                        , uuid, timeout + "");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(result);
        if(result.intValue()==1){ //第一次获取到锁时自动续期
            //走出循环 表示setnx成功  获取到锁，再开启看门狗线程对锁自动续期
            renewLock(lockKey, uuid, timeout);
        }

    }

    //锁的自动续期业务
    private void renewLock(String lockKey, String uuid, int timeout) {
        //使用子线程
//        new Thread(() -> {
//            try {
//                //1、第一次判断是否续期：让子线程休眠 timeout*2/3 秒再执行
//                TimeUnit.SECONDS.sleep(timeout * 2 / 3);
//                //2、判断是否要续期
//                // 到redis中获取lockKey的值 如果 和uuid相等 更新锁的过期时间
//                // 更新成功返回1  其它情况返回0表示失败(watchdog线程可以结束了)
//                String script = " if redis.call('hexists' , KEYS[1] , ARGV[1]) == 1 then return redis.call('expire' , KEYS[1] , ARGV[2]) " +
//                        " else return 0 end";
//                //如果执行更新过期时间的命令返回成功 休眠后 再次执行
//                //如果执行更新过期时间的命令失败，结束循环   watchdog线程正常结束
//                while (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
//                        //class java.lang.Integer cannot be cast to class java.lang.String
//                        Arrays.asList(lockKey), uuid, timeout + "")) { //execute接收的参数只能是字符串类型
//                    TimeUnit.SECONDS.sleep(timeout * 2 / 3);
//                }
//
//                //如果不等，表示当前子线程 要监听的锁业务已经结束 已经删除了 看门狗线程可以结束了
//
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
//
//        }).start();
//
        //通过线程池 执行看门狗线程的任务： 可以控制线程数量 以及复用线程
        ThreadPoolUtils.getPool().execute(()->{
            try {
                //1、第一次判断是否续期：让子线程休眠 timeout*2/3 秒再执行
                TimeUnit.SECONDS.sleep(timeout * 2 / 3);
                //2、判断是否要续期
                // 到redis中获取lockKey的值 如果 和uuid相等 更新锁的过期时间
                // 更新成功返回1  其它情况返回0表示失败(watchdog线程可以结束了)
                String script = " if redis.call('hexists' , KEYS[1] , ARGV[1]) == 1 then return redis.call('expire' , KEYS[1] , ARGV[2]) " +
                        " else return 0 end";
                //如果执行更新过期时间的命令返回成功 休眠后 再次执行
                //如果执行更新过期时间的命令失败，结束循环   watchdog线程正常结束
                while (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
                        //class java.lang.Integer cannot be cast to class java.lang.String
                        Arrays.asList(lockKey), uuid, timeout + "")) { //execute接收的参数只能是字符串类型
                    TimeUnit.SECONDS.sleep(timeout * 2 / 3);
                }

                //如果不等，表示当前子线程 要监听的锁业务已经结束 已经删除了 看门狗线程可以结束了

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
    }

    //释放分布式锁方法
    public void unlock(String lockKey , String uuid) {
        /*
            如果释放的锁是自己的，重入次数-1，如果-1后的结果为0可以删除锁 表示完全释放
         */
        String releaseLockScipt = "if redis.call('exists' , KEYS[1])==0 " +
                "   then return 0 " +
                //锁是自己的
                " elseif redis.call('hincrby' , KEYS[1] , ARGV[1] , -1)==0 " + //锁完全释放
                "   then redis.call('del' , KEYS[1]) return 1" +  //1表示锁释放成功
                " else " + //锁是自己的 释放一次后 使用次数仍然>0 返回2
                "   return 2" +
                " end";
        Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(releaseLockScipt, Long.class),
                Arrays.asList(lockKey), uuid);
        log.info("释放分布式锁的结果: "+ result);
    }
}
